#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/time.h>
#include <time.h>
#include <assert.h>

#include "aget.h"
#include "inet.h"
#include "http.h"


int AGET_DEBUG = 0;

extern const char* appname;
extern int errno;


/* debug output */
void dprintf(char* fmt, ...)
{
    if (AGET_DEBUG) {
        va_list ap;
        va_start(ap, fmt);
        vprintf(fmt, ap);
        va_end(ap);
        fflush(stdout);
    }
}

/* initialize, sets default values to settings. */
int init(void)
{
    if ((agetopt.sck=socket(AF_INET, SOCK_STREAM, 0))==-1) {
        perror(appname);
        abort();
    }
    agetopt.prot    = 0;
    agetopt.uri     = NULL;
    agetopt.host    = NULL;
    agetopt.path    = NULL;
    agetopt.port    = 80;
    agetopt.retry   = 0;
    agetopt.timeout = 60;
    agetopt.offset  = -1;
    agetopt.fp      = stdout;
    agetopt.fh      = NULL;
    agetopt.savecookie = NULL;

    return agetopt.sck;
}

/* free addr if it's not NULL. */
#define FREE(addr) do { if (addr) free(addr); } while (0)

void cleanup(void)
{
    FREE(agetopt.uri);
    FREE(agetopt.host);
    FREE(agetopt.path);

    if (agetopt.fp==agetopt.fh && agetopt.fp!=NULL)
        fclose(agetopt.fp);
    else {
        if (agetopt.fp) fclose(agetopt.fp);
        if (agetopt.fh) fclose(agetopt.fh);
    }
}


/* interpret val as int and set to a. */
void intopt(int* a, const char* val)
{
    if (val==NULL)
        *a = 0;
    else
        *a = atoi(val);
}

/* copy val to a; */
void stringopt(char** a, const char* val)
{
    FREE(*a);
    size_t len = strlen(val) + 1;
    *a = malloc(len);
    if (*a==NULL) {
        perror(appname);
        abort();
    }
    memcpy(*a, val, len);
}

/* set bool option a. */
void boolopt(int* a) { *a = 1; }


/* open a file. first try to open it as r+ and the set the writing pointer
 * to the end of the file, if it failed, then try to open it as w, if it
 * still failed, returns NULL.
 */
FILE* openfile(const char* filename)
{
    if (strlen(filename)==1) {
        if (filename[0]=='-')
            return stdout;
        else if (filename[0]=='*')
            return agetopt.fp;
    }
    
    FILE* fp;
    if ((fp=fopen(filename, "r+"))!=NULL) {
        fseek(fp, 0, SEEK_END);
        return fp;
    }
    return fopen(filename, "w");
}


/*
 * parse url to get protocol, host name, port and path
 * return value indicate the protocol:
 *  -1: unknown.
 *   0: no protocol specified.
 *   1: http
 *   2: https
 *   3: ftp
 */
int parse_uri(char* uri, char** host, unsigned int* port, char** path)
{
    /* parse protocol */
    char* rest = strstr(uri, "://");
    char* prot = NULL;
    if (rest==NULL) /* no protocol found */
        rest = uri;
    else {
        prot = uri;
        rest += 3;
    }

    /* parse path */
    size_t l;
    char* p = strchr(rest, '/');
    if (p==NULL) { /* no path specified */
        *path = malloc(2);
        (*path)[0] = '/';
        (*path)[1] = '\0';
    }
    else {
        l = strlen(p);
        *path = malloc(l+1);
        memcpy(*path, p, l+1);
    }

    /* set host */
    l = p ? p - rest : strlen(rest);
    *host = malloc(l+1);
    memcpy(*host, rest, l);
    (*host)[l] = '\0';

    /* parse port */
    p = strchr(*host, ':');
    if (p!=NULL) { /* port found */
        l = p - *host;
        (*host)[l] = '\0';
        l = atoi(p+1);
        if (l>0) *port=l;
    }

    /* return protocol */
    dprintf("PROTOCOL: ");
    if (prot==NULL) {
        dprintf("DEFAULT(HTTP)\n");
        return 0;
    }
    else if (strncasecmp(prot, "http://", 7)==0) {
        dprintf("HTTP\n");
        return AGET_HTTP;
    }
    else if (strncasecmp(prot, "ftp://", 5)==0) {
        dprintf("FTP\n");
        return AGET_FTP;
    }
    
    dprintf("UNSUPPORTED\n");
    return -1;
}

/* parse 'set-cookie' and save cookie to fp, then returns the length of
 * 'set-cookie' field.
 *
 * \bug it can only deal with first cookie in the same 'Set-Cookie' field,
 *      other cookies will be ignored.
 */
static size_t savecookie(FILE* fp, char* cookie)
{
    char* begin = cookie;
    char tmp1[5]="%xx", tmp2[5];
    cookie += 11;
    while (*cookie==' ' || *cookie=='\t') cookie++;
    while (*cookie!='=') {
        if (*cookie=='%') {
            cookie++;
            tmp1[1] = *cookie++;
            tmp1[2] = *cookie++;
            fprintf(fp, "%s", urldecode(tmp2, 4, tmp1));
        }
        else
            fprintf(fp, "%c", *cookie++);
    }
    fprintf(fp, "\n");
    cookie++; /* ignore '=' */
    while (strchr(",;\r\n", *cookie)==NULL) {
        if (*cookie=='%') {
            cookie++;
            tmp1[1] = *cookie++;
            tmp1[2] = *cookie++;
            fprintf(fp, "%s", urldecode(tmp2, 4, tmp1));
        }
        else
            fprintf(fp, "%c", *cookie++);
    }
    fprintf(fp, "\n");
    if (*cookie=='\r') {
        fprintf(fp, "\n");
        return cookie-begin;
    }
    do cookie++;
    while (*cookie==' ' || *cookie=='\t');
    while (*cookie!='\r')
        fprintf(fp, "%c", *cookie++);
    fprintf(fp, "\n\n");
    
    return cookie-begin;
}

/* perform a http get routine. */
#define BUFFER_SIZE 10240 /* 10kB */
#define READ_LEN 2048 /* 2kB */
static int http_routine(HTTP* http, httpopt_t* hopt)
{
    assert(hopt);
    hopt->host = agetopt.host;
    hopt->port = agetopt.port;
    hopt->path = agetopt.path;
    
        
    char buf[BUFFER_SIZE] = "";
    int n;

    /* add headers */
    http_header(http, "User-Agent: AGET/0.1 (Linux AMD64; en-US)", 0);
    if (agetopt.offset>0) {
        snprintf(buf, BUFFER_SIZE, "Range: bytes=%d-", agetopt.offset);
        http_header(http, buf, 0);
        n = agetopt.offset;
    }
    else
        n = 0;

    /* add data */
    /* follows are examples: */
    /*
    http_data(http, "name", "value");
    http_data(http, "t1", "12ab_ &xyz");
    http_data(http, "中文", "test 吴锡 haha\n\r");
    */

    /* HTTP GET */
    int total, i; /* total bytes, recently arrived bytes */
    total = http_get(http, hopt, buf, BUFFER_SIZE) + n; /* n is the offset */
    if (hopt->request)
        fprintf(hopt->request, "----------------------------\n");
    if (agetopt.fh)
        /* output response header */
        fprintf(agetopt.fh, "%s", buf);
    if (agetopt.fp!=stdout)
        printf("%d\n", total);

    /* dealing with 'set-cookies' */
    char* cookie = buf;
    while ( agetopt.savecookie && (cookie=http_get_cookie(cookie)) ) {
        cookie += savecookie(agetopt.savecookie, cookie);
    }
    
    /* read content */
    size_t len = total<=0 ?       READ_LEN :
                 total<READ_LEN ? total :
                                  READ_LEN;
    while ((i = http_read(http, buf, len))>0) {
        buf[i+1] = 0;
        if (n+i>total) /* IMPORTANT, most times you can recv more data at the
                          end of the file, so this operation is necessary. */
            i = total - n;
        n += i;
        fwrite(buf, i, 1, agetopt.fp);
        if (agetopt.fp!=stdout) { /* output progress */
            printf("\r%d", n);
            fflush(stdout);
        }
        if (total>0) {
            if (agetopt.fp!=stdout) /* output progress */
                printf(" %0.2lf", (double)n/total);
            if (n>=total) break;
        }
    }
    if (agetopt.fp!=stdout) /* output progress */
        printf("\n");

    return 0;
}


/* retrieve data */
int aget(HTTP* http, httpopt_t* hopt)
{
    struct hostent* hp;
    hp = gethostbyname(agetopt.host); /**< \bug: this may not work properly
                                          in some old operation systems,
                                          if param->host is a address. */
    if (hp==NULL)
        return -1; /* failed on gethostbyname */
    if (hp->h_addrtype!=AF_INET) { /* now only support IPv4 */
        errno = EAFNOSUPPORT;
        return -2; /* unknown address type. */
    }

    /* connect: */
    struct sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(agetopt.port);
    memcpy(&servaddr.sin_addr, *hp->h_addr_list, sizeof(servaddr.sin_addr));
    if (hopt->request)
        fprintf(hopt->request, "\nConnecting ... ");
    int n = conn_timeout(agetopt.sck, (const struct sockaddr*)&servaddr,
                         sizeof(servaddr), agetopt.timeout);
    if (n && errno==ETIMEDOUT) return -3; /* connect timeout */
    if (hopt->request)
        fprintf(hopt->request, "OK\n\n");
    
    /* retrieve data according to different protocols:
     * Begin with settings.offset if agetopt.offset<0,
     * otherwise resume getting file.
     */
    int error;
    if (agetopt.prot==AGET_HTTP)
        error = http_routine(http, hopt);
    /* *** HAVEN'T IMPLIMENT YET ***
    else if (agetopt.prot==AGET_FTP)
        error = ftp_routine(fp, sck, uri, pr);
    */

    return errno = error;
}
